import os
import glob
import re

from ruamel import yaml

from ExpressionHandler import ExpressionHandler, CompositeValue, OptionValue, DependencyValue

subdir_joinpath_re = re.compile(r"subdir\(join_paths\((.*?)\)\)", re.MULTILINE)
assignment_re = re.compile(r'(\w+)\s*([+\-]?=|=)\s*(.+)', re.MULTILINE)
library_dic = {}
class MesonDepends(object):
    def __init__(self):
        self.option_helper = MesonOptionDepends()
        self.options = set()
        self.option_dict = {}
        self.commands_dict = {}
        self.option_dep_map = {}
        self.expression_helper = ExpressionHandler(self.options)
        self.main_meson_file = None
        self.main_meson_option_file = None
        self.checked_meson_dirs = set()

        self.guard_stack = GuardStack()

    def parseDir(self, home_dir):
        meson_option_path = os.path.join(home_dir, 'meson_options.txt')
        if os.path.isfile(meson_option_path):
            self.main_meson_option_file = meson_option_path
            self.option_condition_map = self.option_helper.parse_multiple_meson_option(self.option_dict, meson_option_path)
            self.options = self.option_dict.keys()
            self.checked_meson_dirs.add(home_dir)
        else:
            print("warning: no meson_options found!")
        meson_build_path = os.path.join(home_dir, 'meson.build')
        if os.path.isfile(meson_build_path):
            self.main_meson_file = meson_build_path
            self.parse(meson_build_path)
            self.checked_meson_dirs.add(home_dir)
        else:
            print("error: no meson.build found!")

        meson_build_additional_dirs = []
        for root, dirs, files in os.walk(home_dir):
            if 'meson.build' in files:
                meson_build_additional_dirs.append(root)
        for meson_build_additional_dir in meson_build_additional_dirs:
            if meson_build_additional_dir not in self.checked_meson_dirs:
    #             check options first
                meson_addition_option_path = os.path.join(meson_build_additional_dir, 'meson_options.txt')
                if os.path.isfile(meson_addition_option_path):
                    self.option_condition_map = self.option_helper.parse_multiple_meson_option(self.option_dict, meson_addition_option_path)
                    self.options = self.option_dict.keys()
                meson_addition_build_path = os.path.join(meson_build_additional_dir, 'meson.build')
                self.parse(meson_addition_build_path)
                self.checked_meson_dirs.add(meson_build_additional_dir)

    def parse(self, path):
        content = self.to_line(path)
        guard_stack = self.guard_stack
        for line in content:
            if line.startswith('if '):
                self.if_command(line, guard_stack)
            elif line.startswith('elif '):
                self.elif_command(line, guard_stack)
            elif line.startswith('else '):
                self.else_command(line, guard_stack)
            elif line.startswith('endif'):
                self.endif_command(line, guard_stack)
            # elif 'get_option' in line:
            #     self.get_option_command(line)
            # elif 'dependency' in line:
            #     self.dependency_command(line, guard_stack)
            # elif 'find_library' in line:
            #     self.find_library_command(line, guard_stack)
            # elif 'executable' in line:
            #     self.find_executable_command(line, guard_stack)
            elif line.startswith('subdir'):
                self.subdir_command(path, line, guard_stack)
            elif self.expression_helper.is_assignment(line):
                self.analyze_assignment(line, guard_stack)
            else:
                self.analyze_others(line, guard_stack)



    # def is_assignment(self, line):
    #     matches = assignment_re.findall(line)
    #     return len(matches) > 0

    def subdir_command(self, path, line, guard):
        if 'join_paths(' in line:
            matches = subdir_joinpath_re.findall(line)
            for match in matches:
                path_base = path[:path.rfind(os.sep)]
                for dir in match.split(','):
                    dir = dir.strip().strip("'")
                    path_base += os.sep + dir
                new_path = path_base + os.sep + 'meson.build'
                self.checked_meson_dirs.add(path_base)
                self.parse(new_path)
            return None
        else:
            line = line[6:].strip()
            if line[0] != '(':
                return
            if '\'' in line:
                line = line[1: -1].strip()[1:-1].strip()
                path = path[:path.rfind(os.sep)] + os.sep + line + os.sep + 'meson.build'
                print(f'----------------analyze new build file:${path}-------------------')
                self.checked_meson_dirs.add(path.replace(os.sep + 'meson.build', ''))
                self.parse(path)
            else:
                # add all possible folders for analysis
                parent_directory = os.path.dirname(path)
                subfolders = [f.name for f in os.scandir(parent_directory) if f.is_dir()]
                for subfolder in subfolders:
                    new_path = path[:path.rfind(os.sep)] + os.sep + subfolder + os.sep + 'meson.build'
                    if os.path.exists(new_path):
                        print(f'----------------analyze new build file:${new_path}-------------------')
                        self.checked_meson_dirs.add(path.replace(os.sep + 'meson.build', ''))
                        self.parse(new_path)
    def if_command(self, line, guard_stack):

        right_stript_str = line.replace('if ', '').strip()
        right_val = self.expression_helper.get_right_value(right_stript_str)
        if right_val is None:
            current_new_guard = None
        else:
            current_new_guard = right_val.generate_guard()
        guard_stack.update_stack('if', current_new_guard)

    #Note: suppose elif and else will not be used at the same time
    def elif_command(self, line, guard_stack):
        right_stript_str = line.replace('elif ', '').strip()
        right_val = self.expression_helper.get_right_value(right_stript_str)
        if right_val is None:
            current_new_guard = None
        else:
            current_new_guard = right_val.generate_guard()
        guard_stack.update_stack('elif', current_new_guard)

    def else_command(self, line, guard_stack):
        right_stript_str = line.replace('else ', '').strip()
        right_val = self.expression_helper.get_right_value(right_stript_str)
        if right_val is None:
            current_new_guard = None
        else:
            current_new_guard = right_val.generate_guard()
        guard_stack.update_stack('else', current_new_guard)

    def endif_command(self, line, guard_stack):
        guard_stack.update_stack('endif', None)

    def split_param(self, params):
        param = []
        not_split = 0
        last = 0
        for index in range(len(params)):
            if params[index] == '[' or params[index] == '{':
                not_split += 1
            if params[index] == ']' or params[index] == '}':
                not_split -= 1
            if not_split == 0 and params[index] == ',':
                param.append(params[last:index])
                last = index + 1
        param.append(params[last:])
        return param

    def analyze_assignment(self, assignment_str, guard_stack):
        matches = assignment_re.findall(assignment_str)
        # print(assignment_str)
        assignment_str_copy = assignment_str
        for match in matches:
            left = match[0]
            operator = match[1]
            right = match[2]
            left_val = self.expression_helper.get_left_value(left)
            right_val = self.expression_helper.get_right_value(right)
            if operator == '=':
                self.expression_helper.merge_variable(left_val, right_val)
            elif operator == '+=':
                self.expression_helper.add_merge_variable(left_val, right_val)
            self.analyze_options_and_depends_relations(right_val, guard_stack)


    def analyze_others(self, other_str, guard_stack):
        val = self.expression_helper.get_right_value(other_str)
        self.analyze_options_and_depends_relations(val, guard_stack)


    def analyze_options_and_depends_relations(self, analyzed_val, guard_stack):
        guard_exp = guard_stack.get_current_guard()
        #1. option_dict
        contained_options = []
        if analyzed_val is not None:
            contained_options.extend(analyzed_val.get_contained_options())
        if guard_exp is not None:
            contained_options.extend(guard_exp.get_contained_options())
        pass
        if len(contained_options) > 0:
            for option in contained_options:
                assert isinstance(option, OptionValue)
                if option is not None and option.get_val() is not None:
                    option_name = self.remove_quotes(option.get_val())
                    if option_name not in self.option_dict:
                        self.option_dict[option_name] = {"name": option_name}

        #2. option_dep_map
        contained_deps = []
        if analyzed_val is not None:
            contained_deps.extend(analyzed_val.get_contained_depends())
        if len(contained_deps) > 0:
            for dep in contained_deps:
                assert isinstance(dep, DependencyValue)
                if dep.val is None or isinstance(dep.val, list):
                    print('none depdends values')
                dep_tag = dep.type + '.' + self.remove_quotes(dep.val)
                new_guard = guard_exp
                extra_guard = dep.get_required()
                new_guard = self.simplify_guard_pair_by_excluding_deps_guards(new_guard, extra_guard)
                new_guard_to_str = "buildRequires"
                if new_guard is not None:
                    new_guard_str = new_guard.to_simplified_string()
                    if new_guard_str is not None:
                        new_guard_to_str = "buildRequires when (" + new_guard_str + ")"
                if new_guard_to_str not in self.option_dep_map:
                    self.option_dep_map[new_guard_to_str] = []
                if dep_tag not in self.option_dep_map[new_guard_to_str]:
                    self.option_dep_map[new_guard_to_str].append(dep_tag)

    # remove guards only on deps
    def simplify_guard_pair_by_excluding_deps_guards(self, new_guard, extra_guard):
        if new_guard is not None and len(new_guard.get_contained_options()) == 0:
            new_guard = None
        elif extra_guard is not None and len(extra_guard.get_contained_options()) == 0:
            extra_guard = None
        if extra_guard is None and new_guard is None:
            new_guard = None
        elif extra_guard is not None and new_guard is None:
            new_guard = extra_guard
        elif extra_guard is None and new_guard is not None:
            new_guard = new_guard
        else:
            new_guard = CompositeValue.get_and_exp(new_guard, extra_guard)
        return new_guard
    
    # def check_log_string():
    #     log_keywords = ['error', 'warning']
    #     keywords_pattern = '|'.join(log_keywords)
    #     pattern = f'{}'

    def to_line(self, path):
        content = []
        a_line = ''
        left_num = 0
        apostrophe = True
        double_quotes = True
        in_log_line = False
        with open(path, 'r', encoding='utf-8') as meson_build:
            print("content:")
            while True:
                new_line = meson_build.readline()
                if not new_line:
                    break
                if '#' in new_line:
                    new_line = self.is_comment(new_line, apostrophe, double_quotes)
                new_line = new_line.strip()
                if not new_line:
                    continue
                current_in_log_line = False
                if new_line.count("'''") == 1 and ('error(' in new_line or 'warning(' in new_line):
                    current_in_log_line = True
                    in_log_line = True
                elif new_line.count("'''") == 1 and in_log_line:
                    current_in_log_line = True
                    in_log_line = False
                if in_log_line:
                    current_in_log_line = True
                # print("current_in_log_line:", current_in_log_line)
                # print("newline", new_line)
                a_line += new_line
                # print("a_line", a_line)
                pre_charac = None
                for charac in new_line:
                    if charac == "'" and pre_charac != '\\' and not current_in_log_line:
                        apostrophe = not apostrophe
                    if charac == '"':
                        double_quotes = not double_quotes
                    if apostrophe and double_quotes:
                        if charac == '(' or charac == '[' or charac == '{':
                            left_num += 1
                        if charac == ')' or charac == ']' or charac == '}':
                            left_num -= 1
                    pre_charac = charac
                # print("left_num, apostrophe, double_quotes, in_log_line, current_in_log_line:", left_num, apostrophe, double_quotes, in_log_line, current_in_log_line)
                if left_num == 0 and apostrophe and double_quotes:
                    # print(a_line)
                    content.append(a_line)
                    a_line = ''
        if a_line != '': #note: when it fails, output the values anyway
            content.append(a_line)
        return content


    def is_comment(self, line, is_out_apostrophe, is_out_double_quotes):
        if is_out_apostrophe:
            apostrophe = 0
        else:
            apostrophe = 1
        if is_out_double_quotes:
            double_quotes = 0
        else:
            double_quotes = 1
        for index in range(len(line)):
            if line[index] == "'":
                apostrophe += 1
            if line[index] == '"':
                double_quotes += 1
            if line[index] == '#':
                if (apostrophe % 2) == 0 and (double_quotes % 2) == 0:
                    return line[:index]
        return line


    def set_style(self, libs_dict):
        for key in libs_dict:
            libs_dict[key] = list(libs_dict[key])
        return libs_dict

    def save_meson_with_only_depends(self, dir, depends_file_name):
        yml = yaml.YAML()
        yml.width = 4096
        fn = os.path.join(dir, depends_file_name)
        with open(fn, 'w') as file:
            yml.dump(self.option_dep_map, file)

    def save_meson(self, dir):
        # self.set_style(self.libraries_dict)
        yml = yaml.YAML()
        # yml.indent(offset=4)
        yml.width = 4096
        # print(self.libraries_dict)
        # print(self.option_dict)

        option_yml = os.path.join(dir, "options.yaml")
        with open(option_yml, 'w') as file:
            yml.dump(self.option_dict, file)

        # command_yml = os.path.join(dir, "commands.yaml")
        # with open(command_yml, 'w') as file:
        #     yml.dump(self.commands_info, file)

        fn = os.path.join(dir, "depends.yaml")
        # self.libraries_dict.update(self.lib_var_value)
        with open(fn, 'w') as file:
            yml.dump(self.option_dep_map, file)
        #
        # command_yml = os.path.join(dir, "commands.yaml")
        # with open(command_yml, 'w') as file:
        #     yml.dump(self.commands_info, file)
        # final_option_yml = os.path.join(dir, "final_options.yaml")
        # with open(final_option_yml, 'w') as file:
        #     yml.dump(self.final_option(), file)
        # option_set_yml = os.path.join(dir, "option_set.yaml")
        # with open(option_set_yml, 'w') as file:
        #     yml.dump(self.option_condition, file)
        # target_yml = os.path.join(dir, "target.yaml")
        # with open(target_yml, 'w') as file:
        #     yml.dump(self.target_condition, file)

    @staticmethod
    def remove_quotes(given_str):
        return_str = given_str.replace('\'', '').replace('"', '')
        return return_str

class MesonOptionDepends(object):
    def __init__(self):
        pass

    def parse_multiple_meson_option(self, option_dict, file_path):
        with open(file_path, 'r') as file:
            content = file.read()
        options = re.findall(r"option\s*\((.*?)\)", content, re.DOTALL)

        # 解析每个 option 配置
        parsed_options_strs = ["option("+option+")" for option in options]

        # 输出解析结果
        for i, option_str in enumerate(parsed_options_strs, start=1):
            parsed_option = self.parse_single_meson_option(option_str)
            option_dict[parsed_option['name']]={"name": parsed_option['name'],
                                                "type": parsed_option['type'],
                                                "default_value": parsed_option['value'],
                                                "choice": parsed_option['choices'],
                                                "help_text": parsed_option['description']}

    def parse_single_meson_option(self, content):
        name_re = re.compile(r"option\s*\(\s*'([^\']+)'")
        type_re = re.compile(r"type\s*:\s*'(\w+)'")
        value_re = re.compile(r"value\s*:\s*(\[\s*(.*?)\s*\]|\w+)")
        choices_re = re.compile(r"choices\s*:\s*\[(.*?)\]", re.DOTALL)
        description_re = re.compile(r"description\s*:\s*'([^']*)'")

        name_match = name_re.search(content)
        type_match = type_re.search(content)
        value_match = value_re.search(content)
        choices_match = choices_re.search(content)
        description_match = description_re.search(content)

        option_name = name_match.group(1) if name_match else None
        option_type = type_match.group(1) if type_match else None
        if value_match:
            value_str = value_match.group(1)
            if option_type == 'array':
                option_value = [v.strip().strip("'") for v in value_match.group(2).split(',')] if value_match.group(
                    2) else []
            elif option_type == 'boolean':
                option_value = value_str.strip().lower() in ['true', 'false']
            else:
                option_value = value_str.strip()
        else:
            option_value = None
        option_choices = [c.strip().strip("'") for c in choices_match.group(1).split(',')] if choices_match else None
        option_description = description_match.group(1) if description_match else None

        return {
            'name': option_name,
            'type': option_type,
            'value': option_value,
            'choices': option_choices,
            'description': option_description
        }





class GuardStack:
    def __init__(self):
        self.items = [] # should be stack of conditional guard
        self.command_records = []

    def update_stack(self, tag, given_guard):  # it can be "endif", "if", "elif", "else"
        self.command_records.append(tag)
        if tag == "if":
            self.items.append(ConditionalGuard(given_guard))
        elif tag == "elif" or tag == "else":
            current_item = self.items[-1]
            current_item.update_guard(tag, given_guard)
        elif tag == "endif":
            self.items.pop()

    def get_current_guard(self):
        return_guard = None
        for item in self.items:
            current_guard = item.get_current_guard()
            if current_guard is not None:
                if return_guard is not None:
                    return_guard = CompositeValue.get_and_exp(return_guard, current_guard)
                else:
                    return_guard = current_guard
        return return_guard

class ConditionalGuard:
    def __init__(self, given_guard):
        self.if_guard = given_guard
        self.elif_guards = {}
        self.elif_index = -1
        self.current_condition = 'if' # it can be "if", "elif", "else"

    def update_guard(self, tag, given_guard):
        self.current_condition = tag
        if tag == 'elif':
            self.elif_index += 1
            self.elif_guards[self.elif_index] = given_guard
        elif tag == 'else':
            pass # nothing to do

    def get_current_guard(self):
        return_guard = None
        if self.current_condition == 'if':
            return self.if_guard
        elif self.current_condition == 'elif':
            #Note: only returns current 'elif' condition for simplicity
            return self.elif_guards[self.elif_index]
        elif self.current_condition == 'else':
            if self.if_guard is not None:
                return_guard = self.if_guard.get_reverse_clone()
            for guard_id in self.elif_guards:
                current_guard = self.elif_guards[guard_id]
                if current_guard is not None and return_guard is not None:
                    return_guard = CompositeValue.get_and_exp(return_guard, current_guard.get_reverse_clone())
                elif current_guard is None and return_guard is not None:
                    return_guard = return_guard
                elif current_guard is not None and return_guard is None:
                    return_guard = current_guard
                else:
                    return_guard = None
        return return_guard


def get_direct_children(directory):
    files = []
    folders = []

    with os.scandir(directory) as entries:
        for entry in entries:
            if entry.is_file():
                files.append(entry.name)
            elif entry.is_dir():
                folders.append(entry.name)

    return files, folders

if __name__ == '__main__':
    # given_path = os.path.join('package-mapping-results', 'meson_raw_testdata', 'gnome-shell-extension-desktop-icons', 'desktop-icons-20.10.0')
    # meason_dep = MesonDepends()
    # meason_dep.parseDir(given_path)
    # meason_dep.save_meson(given_path)
    # print('-----over-----')



    given_path = 'E:\meson_raw_testdata_single'
    given_files, give_folders = get_direct_children(given_path)
    total_size = len(give_folders)
    current_size = 1
    for project in give_folders:
        project_path = os.path.join(given_path, project)
        inner_given_files, inner_give_folders = get_direct_children(project_path)
        real_project_name = inner_give_folders[0]
        real_project_path = os.path.join(project_path, real_project_name)
        print("----processing file:" + real_project_name + f":{current_size}/{total_size}----")
        meason_dep = MesonDepends()
        meason_dep.parseDir(real_project_path)
        meason_dep.save_meson(real_project_path)
        current_size += 1